<!DOCTYPE HTML>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" /> 
    <title> - 天地维杰网</title>
    <meta name="keywords" content="系统架构,shutdown,不与天斗,Domino,博客,程序员,架构师,笔记,技术,分享,java,Redis">
    
    <meta property="og:title" content="">
    <meta property="og:site_name" content="天地维杰网">
    <meta property="og:image" content="/img/author.jpg"> 
    <meta name="title" content=" - 天地维杰网" />
    <meta name="description" content="天地维杰网 | 博客 | 软件 | 架构 | Java "> 
    <link rel="shortcut icon" href="http://www.shutdown.cn/img/favicon.ico" />
    <link rel="apple-touch-icon" href="http://www.shutdown.cn/img/apple-touch-icon.png" />
    <link rel="apple-touch-icon-precomposed" href="http://www.shutdown.cn/img/apple-touch-icon.png" />
    <link href="http://www.shutdown.cn/js/vendor/font-awesome/css/font-awesome.min.css?v=4.6.2" rel="stylesheet" type="text/css" />
    <link href="http://www.shutdown.cn/js/vendor/fancybox/jquery.fancybox.css?v=2.1.5" rel="stylesheet" type="text/css" />
    <link href="http://www.shutdown.cn/css/main.css" rel="stylesheet" type="text/css" />
    <link href="http://www.shutdown.cn/css/syntax.css" rel="stylesheet" type="text/css" />
    <script type="text/javascript" id="hexo.configuration">
  var NexT = window.NexT || {};
  var CONFIG = {
    scheme: 'Pisces',
    sidebar: {"position":"left","display":"post"},
     fancybox: true, 
    motion: true
  };
</script>
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-7826003325059020" crossorigin="anonymous"></script>
</head>
<body itemscope itemtype="http://schema.org/WebPage" lang="zh-Hans">
<div class="container one-collumn sidebar-position-left page-home  ">
    <div class="headband"></div>

    <header id="header" class="header" itemscope itemtype="http://schema.org/WPHeader">
      <div class="header-inner"> <div class="site-meta  custom-logo ">

  <div class="custom-logo-site-title">
    <a href="http://www.shutdown.cn"  class="brand" rel="start">
      <span class="logo-line-before"><i></i></span>
      <span class="site-title">天地维杰网</span>
      <span class="logo-line-after"><i></i></span>
    </a>
  </div>
  <p class="site-subtitle">人如秋鸿来有信，事若春梦了无痕</p>
</div>

<div class="site-nav-toggle">
  <button>
    <span class="btn-bar"></span>
    <span class="btn-bar"></span>
    <span class="btn-bar"></span>
  </button>
</div>

<nav class="site-nav">
    <ul id="menu" class="menu">
      
      
        <li class="menu-item ">
          <a href="http://www.shutdown.cn/" rel="section">
              <i class="menu-item-icon fa fa-fw fa-home"></i> <br />首页
          </a>
        </li>
      
        <li class="menu-item ">
          <a href="http://www.shutdown.cn/categories/redis/" rel="section">
              <i class="menu-item-icon fa fa-fw fa-battery-full"></i> <br />Redis
          </a>
        </li>
      
        <li class="menu-item ">
          <a href="http://www.shutdown.cn/categories/java/" rel="section">
              <i class="menu-item-icon fa fa-fw fa-coffee"></i> <br />java
          </a>
        </li>
      
        <li class="menu-item ">
          <a href="http://www.shutdown.cn/categories/linux/" rel="section">
              <i class="menu-item-icon fa fa-fw fa-linux"></i> <br />linux
          </a>
        </li>
      
        <li class="menu-item ">
          <a href="http://www.shutdown.cn/categories/daily/" rel="section">
              <i class="menu-item-icon fa fa-fw fa-bug"></i> <br />日常问题
          </a>
        </li>
      
        <li class="menu-item ">
          <a href="http://www.shutdown.cn/categories/spring/" rel="section">
              <i class="menu-item-icon fa fa-fw fa-child"></i> <br />Spring和Springboot
          </a>
        </li>
      
        <li class="menu-item ">
          <a href="http://www.shutdown.cn/categories/mac/" rel="section">
              <i class="menu-item-icon fa fa-fw fa-fire"></i> <br />Mac相关
          </a>
        </li>
      
        <li class="menu-item ">
          <a href="http://www.shutdown.cn/categories/middleware/" rel="section">
              <i class="menu-item-icon fa fa-fw fa-gavel"></i> <br />中间件
          </a>
        </li>
      
        <li class="menu-item ">
          <a href="http://www.shutdown.cn/categories/jiagou/" rel="section">
              <i class="menu-item-icon fa fa-fw fa-rocket"></i> <br />架构
          </a>
        </li>
      
        <li class="menu-item ">
          <a href="http://www.shutdown.cn/categories/python/" rel="section">
              <i class="menu-item-icon fa fa-fw fa-ship"></i> <br />python
          </a>
        </li>
      
        <li class="menu-item ">
          <a href="http://www.shutdown.cn/categories/front/" rel="section">
              <i class="menu-item-icon fa fa-fw fa-bolt"></i> <br />前端
          </a>
        </li>
      
        <li class="menu-item ">
          <a href="http://www.shutdown.cn/categories/jvm/" rel="section">
              <i class="menu-item-icon fa fa-fw fa-balance-scale"></i> <br />jvm
          </a>
        </li>
      
        <li class="menu-item ">
          <a href="http://www.shutdown.cn/categories/c/" rel="section">
              <i class="menu-item-icon fa fa-fw fa-battery-empty"></i> <br />c语言
          </a>
        </li>
      
        <li class="menu-item ">
          <a href="http://www.shutdown.cn/categories/web3/" rel="section">
              <i class="menu-item-icon fa fa-fw fa-web3"></i> <br />web3
          </a>
        </li>
      
        <li class="menu-item ">
          <a href="http://www.shutdown.cn/post/" rel="section">
              <i class="menu-item-icon fa fa-fw fa-archive"></i> <br />归档
          </a>
        </li>
      
        <li class="menu-item ">
          <a href="http://www.shutdown.cn/about/" rel="section">
              <i class="menu-item-icon fa fa-fw fa-user"></i> <br />关于
          </a>
        </li>
      
      <li class="menu-item menu-item-search">
        <a href="javascript:;" class="popup-trigger"> <i class="menu-item-icon fa fa-search fa-fw"></i> <br /> 搜索</a>
      </li>
    </ul>
    <div class="site-search">
      <div class="popup">
 <span class="search-icon fa fa-search"></span>
 <input type="text" id="local-search-input">
 <div id="local-search-result"></div>
 <span class="popup-btn-close">close</span>
</div>

    </div>
</nav>

 </div>
    </header>

    <main id="main" class="main">
      <div class="main-inner">
        <div class="content-wrap">
          <div id="content" class="content">
            
<section id="posts" class="posts-expand">
  <article class="post post-type-normal " itemscope itemtype="http://schema.org/Article">
    <header class="post-header">
      <h1 class="post-title" itemprop="name headline">
        <a class="post-title-link" href="http://www.shutdown.cn/post/redis6.0%E6%96%B0%E7%89%B9%E6%80%A7-%E5%AE%A2%E6%88%B7%E7%AB%AF%E7%BC%93%E5%AD%98/" itemprop="url">
        
        </a>
      </h1>
      <div class="post-meta">
      <span class="post-time">
<span class="post-meta-item-icon">
    <i class="fa fa-calendar-o"></i>
</span>
<span class="post-meta-item-text">时间：</span>
<time itemprop="dateCreated" datetime="2016-03-22T13:04:35+08:00" content="0001-01-01">
    0001-01-01
</time>
</span> 
      
      
       <span>
&nbsp; | &nbsp;
<span class="post-meta-item-icon">
    <i class="fa fa-eye"></i>
</span>
<span class="post-meta-item-text">阅读：</span>
<span class="leancloud-visitors-count">3353 字 ~16分钟</span>
</span>
      </div>
    </header>
    <div class="post-body" itemprop="articleBody">
    

    

<blockquote>
<p>原文地址：<a href="https://redis.io/docs/manual/client-side-caching/">https://redis.io/docs/manual/client-side-caching/</a></p>
</blockquote>

<h1 id="client-side-caching-in-redis">Client-side caching in Redis</h1>

<blockquote>
<p>Redis客户端侧缓存</p>
</blockquote>

<p>Server-assisted, client-side caching in Redis
&gt; Redis服务端支持的客户端侧缓存</p>

<p>Client-side caching is a technique used to create high performance services. It exploits the memory available on application servers, servers that are usually distinct computers compared to the database nodes, to store some subset of the database information directly in the application side.
&gt; 客户端侧缓存是用来创建高性能服务的技术。应用服务器通常与Redis数据库服务器在不同的服务器机器上，客户端缓存可以利用应用服务器的可用内存来数据库信息的子集直接存储到客户端侧。</p>

<p>Normally when data is required, the application servers ask the database about such information, like in the following diagram:</p>

<blockquote>
<p>通常应用服务器会请求数据库服务器获取数据相关信息，如下图所示：</p>
</blockquote>
<div class="highlight"><pre style="background-color:#f8f8f8;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-text" data-lang="text">+-------------+                                +----------+
|             | ------- GET user:1234 -------&gt; |          |
| Application |                                | Database |
|             | &lt;---- username = Alice ------- |          |
+-------------+                                +----------+</code></pre></div>
<p>When client-side caching is used, the application will store the reply of popular queries directly inside the application memory, so that it can reuse such replies later, without contacting the database again:</p>

<blockquote>
<p>使用客户端缓存的话，应用会存储高频请求的返回值到应用内存中，所以应用服务可以复用这些返回值，而不需要再次请求数据库服务器:</p>
</blockquote>
<div class="highlight"><pre style="background-color:#f8f8f8;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-text" data-lang="text">+-------------+                                +----------+
|             |                                |          |
| Application |       ( No chat needed )       | Database |
|             |                                |          |
+-------------+                                +----------+
| Local cache |
|             |
| user:1234 = |
| username    |
| Alice       |
+-------------+</code></pre></div>
<p>While the application memory used for the local cache may not be very big, the time needed in order to access the local computer memory is orders of magnitude smaller compared to accessing a networked service like a database. Since often the same small percentage of data are accessed frequently, this pattern can greatly reduce the latency for the application to get data and, at the same time, the load in the database side.</p>

<blockquote>
<p>虽然用于本地缓存的应用程序内存可能不是很大，但与访问网络服务（如数据库）相比，访问本地计算机内存所需的时间要少几个数量级。由于经常访问相同的一小部分数据，这种模式可以大大减少应用程序获取数据的延迟，同时减少数据库端的负载。</p>
</blockquote>

<p>Moreover there are many datasets where items change very infrequently. For instance, most user posts in a social network are either immutable or rarely edited by the user. Adding to this the fact that usually a small percentage of the posts are very popular, either because a small set of users have a lot of followers and/or because recent posts have a lot more visibility, it is clear why such a pattern can be very useful.</p>

<blockquote>
<p>然而也有很多数据集，其中项目很少变动。例如，在社交网络中，大多数用户的帖子是不可变的或者很少被用户编辑。除此之外，通常只有一小部分帖子非常受欢迎，要么是因为一小部分用户有很多追随者，要么是因为最近的帖子更具可见性，很清楚为什么这样的模式会非常有用。</p>
</blockquote>

<p>Usually the two key advantages of client-side caching are:</p>

<blockquote>
<p>通常客户端缓存的两个关键优势是：</p>
</blockquote>

<ol>
<li>Data is available with a very small latency.
&gt; 数据的延迟很小。</li>
<li>The database system receives less queries, allowing it to serve the same dataset with a smaller number of nodes.
&gt; 可以减少数据库系统接收的查询，因此可以使用较少的节点为同一数据集提供服务。</li>
</ol>

<h2 id="there-are-two-hard-problems-in-computer-science">There are two hard problems in computer science&hellip;</h2>

<blockquote>
<p>计算机科学界有两大难题</p>
</blockquote>

<p>A problem with the above pattern is how to invalidate the information that the application is holding, in order to avoid presenting stale data to the user. For example after the application above locally cached the information for user:1234, Alice may update her username to Flora. Yet the application may continue to serve the old username for user:1234.
&gt; 一个难题是如何废弃应用程序所保存的信息，以避免提供过时的数据给用户。例如，在上面的应用程序中，本地缓存了<code>user:1234</code>的信息，<code>Alice</code>可以更新她的用户名为<code>Flora</code>。但是，应用程序可能他仍然给<code>user:1234</code>提供旧的用户名。</p>

<p>Sometimes, depending on the exact application we are modeling, this isn&rsquo;t a big deal, so the client will just use a fixed maximum &ldquo;time to live&rdquo; for the cached information. Once a given amount of time has elapsed, the information will no longer be considered valid. More complex patterns, when using Redis, leverage the Pub/Sub system in order to send invalidation messages to listening clients. This can be made to work but is tricky and costly from the point of view of the bandwidth used, because often such patterns involve sending the invalidation messages to every client in the application, even if certain clients may not have any copy of the invalidated data. Moreover every application query altering the data requires to use the <a href="https://redis.io/commands/publish"><code>PUBLISH</code></a> command, costing the database more CPU time to process this command.</p>

<blockquote>
<p>有时，依赖于我们建模的精密应用，这个问题不是大问题，所以客户端只需要使用固定的最大“生存时间”来缓存信息。一旦指定的时间已经过去，信息将不再被认为有效。更复杂的模式，当使用Redis时，可以使用Pub/Sub系统来发送废弃消息给侦听的客户端。这虽然也起作用，但是从带宽的角度来说，这很困难且成本高，因为这种模式通常需要发送废弃消息给每个应用程序的客户端，即使某些客户端可能没有任何复制的废弃数据。同时，每个应用程序查询变更都需要使用<a href="https://redis.io/commands/publish"><code>PUBLISH</code></a>命令，而这个命令需要消耗数据库的CPU时间来处理。</p>
</blockquote>

<p>Regardless of what schema is used, there is a simple fact: many very large applications implement some form of client-side caching, because it is the next logical step to having a fast store or a fast cache server. For this reason Redis 6 implements direct support for client-side caching, in order to make this pattern much simpler to implement, more accessible, reliable, and efficient.</p>

<blockquote>
<p>无论使用什么模式，都有一个简单的事实：许多非常大的应用程序实现某种形式的客户端缓存，因为这是拥有快速存储或快速缓存服务器的下一个逻辑步骤。因此，Redis 6实现了对客户端缓存的直接支持，以使此模式更易于实现、更易访问、更可靠和更高效。</p>
</blockquote>

<h2 id="the-redis-implementation-of-client-side-caching">The Redis implementation of client-side caching</h2>

<p>The Redis client-side caching support is called <em>Tracking</em>, and has two modes:</p>

<blockquote>
<p>Redis客户端缓存支持被称为*追踪*，其有两种模式：</p>
</blockquote>

<ul>
<li><p>In the default mode, the server remembers what keys a given client accessed, and sends invalidation messages when the same keys are modified. This costs memory in the server side, but sends invalidation messages only for the set of keys that the client might have in memory.
&gt; 默认模式下，服务端会记住客户端访问的键，并在键被修改时发送废弃消息。这在服务器端的内存中消耗了一些空间，但是只发送客户端可能在内存中的键的废弃消息。</p></li>

<li><p>In the <em>broadcasting</em> mode, the server does not attempt to remember what keys a given client accessed, so this mode costs no memory at all in the server side. Instead clients subscribe to key prefixes such as <code>object:</code> or <code>user:</code>, and receive a notification message every time a key matching a subscribed prefix is touched.
&gt; 在*广播*模式下，服务端不会尝试记住客户端访问的键，因此在服务器端的内存中消耗不到任何空间。而客户端通过订阅前缀如<code>object:</code>或<code>user:</code>，并在键匹配订阅前缀时接收通知消息。</p></li>
</ul>

<p>To recap, for now let&rsquo;s forget for a moment about the broadcasting mode, to focus on the first mode. We&rsquo;ll describe broadcasting later more in details.</p>

<blockquote>
<p>在这里，我们将忽略广播模式，以关注第一模式。我们将在后续更详细地描述广播模式。</p>
</blockquote>

<ol>
<li>Clients can enable tracking if they want. Connections start without tracking enabled.
&gt; 客户端可以启用追踪，连接启动时不启用追踪。</li>
<li>When tracking is enabled, the server remembers what keys each client requested during the connection lifetime (by sending read commands about such keys).
&gt; 当追踪开启时，服务端会记住每个客户端在连接生命周期内请求的键（通过发送这些键的读取命令实现）。</li>
<li>When a key is modified by some client, or is evicted because it has an associated expire time, or evicted because of a <em>maxmemory</em> policy, all the clients with tracking enabled that may have the key cached, are notified with an <em>invalidation message</em>.
&gt; 当一个key被某些客户端修改，或因为超时而被淘汰，或因为*maxmemory*（最大内存）策略而被淘汰时，所有开启追踪并可能缓存该key的客户端，都会收到<em>invalidation message</em> (无效信息)的通知。</li>
<li>When clients receive invalidation messages, they are required to remove the corresponding keys, in order to avoid serving stale data.
&gt; 当客户端收到无效信息时，他们需要删除相应的键，以避免使用过期的数据。</li>
</ol>

<p>This is an example of the protocol:
    &gt; 一下一个协议的例子：</p>

<ul>
<li>Client 1 <code>-&gt;</code> Server: CLIENT TRACKING ON
&gt; 客户端1 <code>-&gt;</code> 服务器：CLIENT TRACKING ON</li>
<li>Client 1 <code>-&gt;</code> Server: GET foo</li>
<li>(The server remembers that Client 1 may have the key &ldquo;foo&rdquo; cached)
&gt; (服务器记住Client 1可能缓存键&rdquo;foo&rdquo;)</li>
<li>(Client 1 may remember the value of &ldquo;foo&rdquo; inside its local memory)
&gt; (Client 1可能在本地内存中记住键&rdquo;foo&rdquo;的值)</li>
<li>Client 2 <code>-&gt;</code> Server: SET foo SomeOtherValue
&gt; (Client 2发送SET命令，并且把键&rdquo;foo&rdquo;的值设置为&rdquo;SomeOtherValue&rdquo;)</li>
<li>Server <code>-&gt;</code> Client 1: INVALIDATE &ldquo;foo&rdquo;
&gt; (服务器向Client 1发送废弃消息，把键&rdquo;foo&rdquo;的缓存废弃)</li>
</ul>

<p>This looks great superficially, but if you imagine 10k connected clients all asking for millions of keys over long living connection, the server ends up storing too much information. For this reason Redis uses two key ideas in order to limit the amount of memory used server-side and the CPU cost of handling the data structures implementing the feature:</p>
<div class="highlight"><pre style="background-color:#f8f8f8;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-fallback" data-lang="fallback">&gt; 这看起来很肤浅，但假设 10k 个连接的客户端每个活跃的长连接都要请求 10W 个键，服务器会因为存储太多信息而难以承担。为了限制服务端的内存使用量和CPU消耗，Redis使用两种键思路来实现功能：</code></pre></div>
<ul>
<li>The server remembers the list of clients that may have cached a given key in a single global table. This table is called the <strong>Invalidation Table</strong>. The invalidation table can contain a maximum number of entries. If a new key is inserted, the server may evict an older entry by pretending that such key was modified (even if it was not), and sending an invalidation message to the clients. Doing so, it can reclaim the memory used for this key, even if this will force the clients having a local copy of the key to evict it.
&gt; 服务端将可能缓存了key的客户端到一个单独的全局表中。这个表可以被成为 <strong>无效信息表</strong>。无效信息表可以包含最大条目数。如果一个新键被插入，服务器会淘汰一个旧的数据条目，并且向客户端发送无效信息。这样做，它可以释放用于该键的内存，即使这会强制客户端有本地复制的键淘汰该键。</li>
<li>Inside the invalidation table we don&rsquo;t really need to store pointers to clients&rsquo; structures, that would force a garbage collection procedure when the client disconnects: instead what we do is just store client IDs (each Redis client has a unique numerical ID). If a client disconnects, the information will be incrementally garbage collected as caching slots are invalidated.
&gt; 在无效信息表中，我们不需要存储客户端的结构指针(存指针的话当客户端断开连接时会强制开启一个垃圾回收程序)：取而代之的是，我们只存储客户端ID（每个客户端都有一个卫衣的数字id）。如果客户端断连，随着缓存槽位被废弃，客户端信息会增量地进行垃圾回收。</li>
<li>There is a single keys namespace, not divided by database numbers. So if a client is caching the key <code>foo</code> in database 2, and some other client changes the value of the key <code>foo</code> in database 3, an invalidation message will still be sent. This way we can ignore database numbers reducing both the memory usage and the implementation complexity.
&gt; 单独的不区分数据库号的键空间。如果一个客户端缓存可db2的key <code>foo</code>，其他客户端缓存了db3的key <code>koo</code>，那么无效信息仍然会被发送。这样做我们可以忽略数据库号，减少内存使用和实现复杂性。</li>
</ul>

<h2 id="two-connections-mode">Two connections mode</h2>

<blockquote>
<p>两种连接模式</p>
</blockquote>

<p>Using the new version of the Redis protocol, RESP3, supported by Redis 6, it is possible to run the data queries and receive the invalidation messages in the same connection. However many client implementations may prefer to implement client-side caching using two separated connections: one for data, and one for invalidation messages. For this reason when a client enables tracking, it can specify to redirect the invalidation messages to another connection by specifying the &ldquo;client ID&rdquo; of a different connection. Many data connections can redirect invalidation messages to the same connection, this is useful for clients implementing connection pooling. The two connections model is the only one that is also supported for RESP2 (which lacks the ability to multiplex different kind of information in the same connection).</p>

<blockquote>
<p>使用Redis6支持的新版本的Redis协议 RESP3 ，可以同时在一个连接中进行数据查询和接收无效信息。很多客户端实现可能会更倾向于使用两个单独的连接实现客户端缓存：一个连接处理数据，一个单独接收无效信息。因此当一个客户端开启了追踪，它可以通过指定一个不同的连接的“客户端ID”来重定向无效信息。多个数据连接可以重定向无效信息到同一个连接，这对于实现连接池的客户端非常有用。<code>双连接模型</code>是唯一支持的 RESP2的 （它没有能力在相同连接中多路复用不同类型的信息）。</p>
</blockquote>

<p>Here&rsquo;s an example of a complete session using the Redis protocol in the old RESP2 mode involving the following steps: enabling tracking redirecting to another connection, asking for a key, and getting an invalidation message once the key gets modified.</p>

<blockquote>
<p>举个例子，使用RESP2协议模式的完整回话包含以下步骤：开启追踪重定向到另一个连接，请求一个键，并在键被修改时获得一个无效信息。</p>
</blockquote>

<p>To start, the client opens a first connection that will be used for invalidations, requests the connection ID, and subscribes via Pub/Sub to the special channel that is used to get invalidation messages when in RESP2 modes (remember that RESP2 is the usual Redis protocol, and not the more advanced protocol that you can use, optionally, with Redis 6 using the <a href="https://redis.io/commands/hello"><code>HELLO</code></a> command):</p>

<blockquote>
<p>开始，客户端开启一个连接用于无效信息，请求连接ID，并通过Pub/Sub订阅特定的频道，用于在RESP2模式下获得无效信息（记住，RESP2是Redis原本的协议，而不是更高级的协议，可以在Redis6使用<a href="https://redis.io/commands/hello"><code>HELLO</code></a>命令）：</p>
</blockquote>
<div class="highlight"><pre style="background-color:#f8f8f8;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-shell" data-lang="shell"><span style="color:#ce5c00;font-weight:bold">(</span>Connection <span style="color:#0000cf;font-weight:bold">1</span> -- used <span style="color:#204a87;font-weight:bold">for</span> invalidations<span style="color:#ce5c00;font-weight:bold">)</span>
CLIENT ID
:4
SUBSCRIBE __redis__:invalidate
*3
<span style="color:#000">$9</span>
subscribe
<span style="color:#000">$20</span>
__redis__:invalidate
:1</code></pre></div>
<p>Now we can enable tracking from the data connection:
&gt; 现在我们可以从数据连接开启追踪：</p>
<div class="highlight"><pre style="background-color:#f8f8f8;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-fallback" data-lang="fallback">(Connection 2 -- data connection)
CLIENT TRACKING on REDIRECT 4
+OK

GET foo
$3
bar</code></pre></div>
<p>The client may decide to cache <code>&quot;foo&quot; =&gt; &quot;bar&quot;</code> in the local memory.</p>

<blockquote>
<p>客户端可以决定缓存<code>&quot;foo&quot; =&gt; &quot;bar&quot;</code>在本地内存中。</p>
</blockquote>

<p>A different client will now modify the value of the &ldquo;foo&rdquo; key:</p>

<blockquote>
<p>一个不同的客户端将修改&rdquo;foo&rdquo;键的值：</p>
</blockquote>
<div class="highlight"><pre style="background-color:#f8f8f8;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-shell" data-lang="shell"><span style="color:#ce5c00;font-weight:bold">(</span>Some other unrelated connection<span style="color:#ce5c00;font-weight:bold">)</span>
SET foo bar
+OK</code></pre></div>
<p>As a result, the invalidations connection will receive a message that invalidates the specified key.</p>

<blockquote>
<p>结果，无效信息连接将接收一个消息，使作废指定的键。</p>
</blockquote>
<div class="highlight"><pre style="background-color:#f8f8f8;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-shell" data-lang="shell"><span style="color:#ce5c00;font-weight:bold">(</span>Connection <span style="color:#0000cf;font-weight:bold">1</span> -- used <span style="color:#204a87;font-weight:bold">for</span> invalidations<span style="color:#ce5c00;font-weight:bold">)</span>
*3
<span style="color:#000">$7</span>
message
<span style="color:#000">$20</span>
__redis__:invalidate
*1
<span style="color:#000">$3</span>
foo</code></pre></div>
<p>The client will check if there are cached keys in this caching slot, and will evict the information that is no longer valid.</p>

<blockquote>
<p>客户端将检查这个缓存槽中是否有缓存的键，并将移除不再有效的信息。</p>
</blockquote>

<p>Note that the third element of the Pub/Sub message is not a single key but is a Redis array with just a single element. Since we send an array, if there are groups of keys to invalidate, we can do that in a single message. In case of a flush (<a href="https://redis.io/commands/flushall"><code>FLUSHALL</code></a> or <a href="https://redis.io/commands/flushdb"><code>FLUSHDB</code></a>), a <code>null</code> message will be sent.</p>

<blockquote>
<p>注意：PUB/SUB消息的第三个元素不是一个单独的key，而是一个只有一个元素的Redis数组。因为我们发送一个数组，如果有组的键要作废，我们也可以在一个消息完成。如果是一个flush（<a href="https://redis.io/commands/flushall"><code>FLUSHALL</code></a>或<a href="https://redis.io/commands/flushdb"><code>FLUSHDB</code></a>），将会发送一个<code>null</code>消息。</p>
</blockquote>

<p>A very important thing to understand about client-side caching used with RESP2 and a Pub/Sub connection in order to read the invalidation messages, is that using Pub/Sub is entirely a trick <strong>in order to reuse old client implementations</strong>, but actually the message is not really sent to a channel and received by all the clients subscribed to it. Only the connection we specified in the <code>REDIRECT</code> argument of the <a href="https://redis.io/commands/client"><code>CLIENT</code></a> command will actually receive the Pub/Sub message, making the feature a lot more scalable.</p>

<blockquote>
<p>关于客户端使用RESP2和PUB/SUB链接来读取无效无效信息的细节，很重要的一个事情是，使用<strong>PUB/SUB是一个复用旧客户端实现的一个套路</strong>，但实际上，消息并不发送到一个频道然后让所有订阅这个频道的客户端接收消息。只有我们定义在<code>CLIENT</code>命令的<code>REDIRECT</code>参数的连接才会真正接收到Pub/Sub消息，这使得功能非常灵活。</p>
</blockquote>

<p>When RESP3 is used instead, invalidation messages are sent (either in the same connection, or in the secondary connection when redirection is used) as <code>push</code> messages (read the RESP3 specification for more information).</p>

<blockquote>
<p>当使用协议替换为RESP3时，无效信息作为<code>push</code>消息进行发送（在相同的连接或者在重定向时使用的第二个连接）（请阅读RESP3说明）。</p>
</blockquote>

<h2 id="what-tracking-tracks">What tracking tracks</h2>

<blockquote>
<p>&ldquo;追踪&rdquo;追踪了什么</p>
</blockquote>

<p>As you can see clients do not need, by default, to tell the server what keys they are caching. Every key that is mentioned in the context of a read-only command is tracked by the server, because it <em>could be cached</em>.</p>

<blockquote>
<p>客户端无需告诉缓存服务端他们缓存了哪些键。每个只读命令用到的key都会被服务器追踪，因为它*可能被缓存*。</p>
</blockquote>

<p>This has the obvious advantage of not requiring the client to tell the server what it is caching. Moreover in many clients implementations, this is what you want, because a good solution could be to just cache everything that is not already cached, using a first-in first-out approach: we may want to cache a fixed number of objects, every new data we retrieve, we could cache it, discarding the oldest cached object. More advanced implementations may instead drop the least used object or alike.</p>

<blockquote>
<p>不需相服务端告知客户端缓存了哪些键有明显的优点。此外，在许多客户端实现中，这正是您想要的，因为用来缓存尚未缓存的所有内容的一个好的解决方案是使用先进先出的方法：我们希望缓存固定数量的对象，我们检索到的每一个新数据，我们都可以缓存它，丢弃最旧的缓存对象。更高级的实现可能会丢弃使用最少的对象或类似对象。</p>
</blockquote>

<p>Note that anyway if there is write traffic on the server, caching slots will get invalidated during the course of the time. In general when the server assumes that what we get we also cache, we are making a tradeoff:</p>

<blockquote>
<p>注意，如果服务端有写入操作，在这个过程中客户端的缓存槽将会被无效化。一般情况下，当服务器端认为我们收到的是我们缓存的内容时，我们就做了一个权衡：</p>
</blockquote>

<ol>
<li>It is more efficient when the client tends to cache many things with a policy that welcomes new objects.
&gt; 当客户端使用迎新策略（应该就是上段提到的先进先出方案）来缓存大量的对象时更加高效。</li>
<li>The server will be forced to retain more data about the client keys.
&gt; 服务器将强制保留更多关于客户端键的数据。</li>
<li>The client will receive useless invalidation messages about objects it did not cache.
&gt; 客户端将收到无用的无效消息，因为它没有缓存这些对象。</li>
</ol>

<p>So there is an alternative described in the next section.</p>

<h2 id="opt-in-caching">Opt-in caching</h2>

<blockquote>
<p>可选缓存</p>
</blockquote>

<p>Clients implementations may want to cache only selected keys, and communicate explicitly to the server what they&rsquo;ll cache and what they will not. This will require more bandwidth when caching new objects, but at the same time reduces the amount of data that the server has to remember and the amount of invalidation messages received by the client.</p>

<blockquote>
<p>客户端可能指向缓存指定的Key，并且通过明确的方式告诉服务器要缓存哪些键，以及不要缓存哪些键。这将需要更多的带宽来缓存新的对象，但同时也会减少服务器记住的数据量和客户端收到的无效消息的数量。</p>
</blockquote>

<p>In order to do this, tracking must be enabled using the OPTIN option:</p>

<blockquote>
<p>为了实现这一点，必须使用OPTIN选项：</p>
</blockquote>
<div class="highlight"><pre style="background-color:#f8f8f8;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-shell" data-lang="shell">CLIENT TRACKING on REDIRECT <span style="color:#0000cf;font-weight:bold">1234</span> OPTIN</code></pre></div>
<p>In this mode, by default, keys mentioned in read queries <em>are not supposed to be cached</em>, instead when a client wants to cache something, it must send a special command immediately before the actual command to retrieve the data:</p>

<blockquote>
<p>在这种模式下，默认情况下，在读查询中提到的键*不应该被缓存*，而当客户端想要缓存某些内容时，它必须在实际发送获取数据的命令之前发送一个特殊的命令：</p>
</blockquote>
<div class="highlight"><pre style="background-color:#f8f8f8;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-shell" data-lang="shell">CLIENT CACHING YES
+OK
GET foo
<span style="color:#4e9a06">&#34;bar&#34;</span></code></pre></div>
<p>The <code>CACHING</code> command affects the command executed immediately after it, however in case the next command is <a href="https://redis.io/commands/multi"><code>MULTI</code></a>, all the commands in the transaction will be tracked. Similarly in case of Lua scripts, all the commands executed by the script will be tracked.</p>

<p><code>CACHING</code> 命令会影响到它后面立刻执行的命令，但是后续的命令是 <a href="https://redis.io/commands/multi"><code>MULTI</code></a>的情况，所有事务中的命令都会被跟踪。同样在Lua甲苯中，所有脚本中的执行的命令都会被跟踪。</p>

<h2 id="broadcasting-mode">Broadcasting mode</h2>

<blockquote>
<p>广播模式</p>
</blockquote>

<p>So far we described the first client-side caching model that Redis implements. There is another one, called broadcasting, that sees the problem from the point of view of a different tradeoff, does not consume any memory on the server side, but instead sends more invalidation messages to clients. In this mode we have the following main behaviors:</p>

<blockquote>
<p>现在说说广播模式，不在服务端缓存数据，而是发送更多的无效消息到客户端。这种模式下有以下主要行为：</p>
</blockquote>

<ul>
<li>Clients enable client-side caching using the <code>BCAST</code> option, specifying one or more prefixes using the <code>PREFIX</code> option. For instance: <code>CLIENT TRACKING on REDIRECT 10 BCAST PREFIX object: PREFIX user:</code>. If no prefix is specified at all, the prefix is assumed to be the empty string, so the client will receive invalidation messages for every key that gets modified. Instead if one or more prefixes are used, only keys matching one of the specified prefixes will be sent in the invalidation messages.
&gt; 客户端使用<code>BCAST</code>条件开启客户端缓存，使用<code>PREFIX</code>条件指定一个或多个前缀。例：<code>CLIENT TRACKING on REDIRECT 10 BCAST PREFIX objext: PREFIX user:</code>。如果没有指定前缀，那么前缀被设定为空字符串，客户端会接收所有key变更后的的无效信息。如果指定了一个或多个前缀，只有匹配相应前缀的key会被发送无效信息到客户端。</li>
<li>The server does not store anything in the invalidation table. Instead it uses a different <strong>Prefixes Table</strong>, where each prefix is associated to a list of clients.
&gt; 服务端不回在无效信息表中存储数据，而是使用另外的<strong>前缀表</strong>，表中的每个前缀都会和一个客户端列表关联起来。</li>
<li>No two prefixes can track overlapping parts of the keyspace. For instance, having the prefix &ldquo;foo&rdquo; and &ldquo;foob&rdquo; would not be allowed, since they would both trigger an invalidation for the key &ldquo;foobar&rdquo;. However, just using the prefix &ldquo;foo&rdquo; is sufficient.
&gt; 两个前缀不能同时跟踪相互重叠的部分，例如不允许同时有前缀&rdquo;foo&rdquo;和&rdquo;foob&rdquo;，因为这两个前缀都会触发一个key为&rdquo;foobar&rdquo;的无效信息。但是只使用前缀&rdquo;foo&rdquo;就够了。</li>
<li>Every time a key matching any of the prefixes is modified, all the clients subscribed to that prefix, will receive the invalidation message.
&gt; 每次匹配到了任意一个前缀的key有所变更，那么所有订阅了这个前缀的客户端都会收到无效信息。</li>
<li>The server will consume CPU proportional to the number of registered prefixes. If you have just a few, it is hard to see any difference. With a big number of prefixes the CPU cost can become quite large.
&gt; 服务器消耗的CPU资源与注册的前缀数量成正比。如果你只有几个前缀，那么没有什么差别。如果你有一大堆前缀，那么CPU资源消耗可能会很大。</li>
<li>In this mode the server can perform the optimization of creating a single reply for all the clients subscribed to a given prefix, and send the same reply to all. This helps to lower the CPU usage.
&gt; 在此模式下，服务器可以执行优化，为订阅了给定前缀的所有客户端创建单个回复，并向所有客户端发送相同的回复。这有助于降低CPU使用率。</li>
</ul>

<h2 id="the-noloop-option">The NOLOOP option</h2>

<blockquote>
<p>NOLOOP选项</p>
</blockquote>

<p>By default client-side tracking will send invalidation messages to the client that modified the key. Sometimes clients want this, since they implement very basic logic that does not involve automatically caching writes locally. However, more advanced clients may want to cache even the writes they are doing in the local in-memory table. In such case receiving an invalidation message immediately after the write is a problem, since it will force the client to evict the value it just cached.</p>

<blockquote>
<p>默认情况下客户端侧追踪会发送无效信息到修改了key的客户端。有时客户端需要这种行为，因为他们实现的是非常简单的逻辑，没有自动缓存本地写入。但是更高级的客户端可能需要缓存他们在本地内存中执行的写入。在这种情况下，写入后立即接收到一个无效信息会问题，因为它会强制客户端强制回收它刚刚缓存的值。</p>
</blockquote>

<p>In this case it is possible to use the <code>NOLOOP</code> option: it works both in normal and broadcasting mode. Using this option, clients are able to tell the server they don&rsquo;t want to receive invalidation messages for keys that they modified.</p>

<blockquote>
<p>这种情况下可以使用<code>NOLOOP</code>条件:在普通模式和广播模式都可以使用。使用这个选项后，客户端会告知服务端他们不需要接收他们自己修改的key的无效信息。</p>
</blockquote>

<h2 id="avoiding-race-conditions">Avoiding race conditions</h2>

<blockquote>
<p>避免竞争条件</p>
</blockquote>

<p>When implementing client-side caching redirecting the invalidation messages to a different connection, you should be aware that there is a possible race condition. See the following example interaction, where we&rsquo;ll call the data connection &ldquo;D&rdquo; and the invalidation connection &ldquo;I&rdquo;:
&gt; 当实现了客户端缓存转发无效消息到其他连接时，你要有一个竞争条件。如下例的加护，我们把数据连接叫做&rdquo;D&rdquo;，无效消息连接叫做&rdquo;I&rdquo;:</p>
<div class="highlight"><pre style="background-color:#f8f8f8;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-text" data-lang="text">[D] client -&gt; server: GET foo
##客户端想服务端发送 GET foo请求
[I] server -&gt; client: Invalidate foo (somebody else touched it)
##服务端发送无效消息给客户端（有其他连接修改了foo这个Key）
[D] server -&gt; client: &#34;bar&#34; (the reply of &#34;GET foo&#34;)
##服务端发送回复给客户端（&#34;GET foo&#34;的回复）</code></pre></div>
<p>As you can see, because the reply to the GET was slower to reach the client, we received the invalidation message before the actual data that is already no longer valid. So we&rsquo;ll keep serving a stale version of the foo key. To avoid this problem, it is a good idea to populate the cache when we send the command with a placeholder:
&gt; 如你所见，因为对GET的回复慢了，我们在数据无效之前接收到了无效消息。所以我们会一致使用foo这个键值的旧版本。为了避免这个问题，我们可以在发送命令时使用占位符：</p>
<div class="highlight"><pre style="background-color:#f8f8f8;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-text" data-lang="text">Client cache: set the local copy of &#34;foo&#34; to &#34;caching-in-progress&#34;
##客户端缓存：设置本地的&#34;foo&#34;的副本值为&#34;caching-in-progress&#34;
[D] client-&gt; server: GET foo.
##[D]客户端向服务端发送 GET foo请求.
[I] server -&gt; client: Invalidate foo (somebody else touched it)
##[I]服务端发送无效消息给客户端（有其他连接修改了foo这个Key）
Client cache: delete &#34;foo&#34; from the local cache.
##客户端缓存：删除本地的&#34;foo&#34;的副本。
[D] server -&gt; client: &#34;bar&#34; (the reply of &#34;GET foo&#34;)
##[D]服务端发送回复给客户端（&#34;GET foo&#34;的回复）
Client cache: don&#39;t set &#34;bar&#34; since the entry for &#34;foo&#34; is missing.
##客户端缓存：不设置&#34;bar&#34;，因为&#34;foo&#34;的副本不存在。</code></pre></div>
<p>Such a race condition is not possible when using a single connection for both data and invalidation messages, since the order of the messages is always known in that case.</p>

<blockquote>
<p>如果数据连接和无效消息连接使用同一连接，那么这个竞争条件就不成立了。因为在这种情况下，消息的顺序是确定的。</p>
</blockquote>

<h2 id="what-to-do-when-losing-connection-with-the-server">What to do when losing connection with the server</h2>

<blockquote>
<p>丢失与服务器的连接时咋整？</p>
</blockquote>

<p>Similarly, if we lost the connection with the socket we use in order to get the invalidation messages, we may end with stale data. In order to avoid this problem, we need to do the following things:
&gt; 同样，如果我们丢失了我们用于获取无效消息的socket，我们可能会出现脏数据。为了避免这个问题，我们需要做以下的事情：</p>

<ol>
<li>Make sure that if the connection is lost, the local cache is flushed.
&gt; 如果连接丢失，我们需要清空本地缓存。</li>
<li>Both when using RESP2 with Pub/Sub, or RESP3, ping the invalidation channel periodically (you can send PING commands even when the connection is in Pub/Sub mode!). If the connection looks broken and we are not able to receive ping backs, after a maximum amount of time, close the connection and flush the cache.
&gt; 无论使用RESP2+Pub/Sub模式时，还是使用RESP3，都需要定期ping 无效信息频道。（即使是Pub/Sub模式下的连接也能发送PING命令）。如果连接看起来像是坏的，并且我们不能接收到PING回复，那么在一定时间内，我们需要关闭连接并且清空缓存。</li>
</ol>

<h2 id="what-to-cache">What to cache</h2>

<blockquote>
<p>缓存什么</p>
</blockquote>

<p>Clients may want to run internal statistics about the number of times a given cached key was actually served in a request, to understand in the future what is good to cache. In general:</p>

<blockquote>
<p>客户端可能想要运行关于给定缓存key在一次请求中实际被使用的次数的内部统计，以了解以后缓存什么好。通常：</p>
</blockquote>

<ul>
<li>We don&rsquo;t want to cache many keys that change continuously.
&gt; 我们要缓存一致变化的Key</li>
<li>We don&rsquo;t want to cache many keys that are requested very rarely.
&gt; 我们不向缓存很少访问的key</li>
<li>We want to cache keys that are requested often and change at a reasonable rate. For an example of key not changing at a reasonable rate, think of a global counter that is continuously <a href="https://redis.io/commands/incr"><code>INCR</code></a>emented.
&gt; 我们想缓存频繁访问并且变更率合理的key。举个变更率不合理的例子，一个一直增长的全局计数器。</li>
</ul>

<p>However simpler clients may just evict data using some random sampling just remembering the last time a given cached value was served, trying to evict keys that were not served recently.</p>

<blockquote>
<p>但是简单的客户端可能只使用随机采样来记住给定缓存值最近一次被访问的时间，尝试删除最近没有被访问的key。</p>
</blockquote>

<h2 id="other-hints-for-implementing-client-libraries">Other hints for implementing client libraries</h2>

<blockquote>
<p>客户端库的其他提示</p>
</blockquote>

<ul>
<li>Handling TTLs: make sure you also request the key TTL and set the TTL in the local cache if you want to support caching keys with a TTL.
&gt; 处理TTL: 如果你需要支持缓存key的TTL，那你要确保你会获取KEY的TTL并缓存到本地缓存中。</li>
<li>Putting a max TTL on every key is a good idea, even if it has no TTL. This protects against bugs or connection issues that would make the client have old data in the local copy.
&gt; 最好给每个KEY设置一个最大的TTL，即使它没有TTL。这可以防止因为的bug或连接问题导致客户端在本地缓存中存储旧数据的问题。</li>
<li>Limiting the amount of memory used by clients is absolutely needed. There must be a way to evict old keys when new ones are added.
&gt; 限制客户端使用的内存。当新数据被添加时，必须有一种方法来删除旧数据。</li>
</ul>

<h2 id="limiting-the-amount-of-memory-used-by-redis">Limiting the amount of memory used by Redis</h2>

<blockquote>
<p>限制Redis使用的内存</p>
</blockquote>

<p>Be sure to configure a suitable value for the maximum number of keys remembered by Redis or alternatively use the BCAST mode that consumes no memory at all on the Redis side. Note that the memory consumed by Redis when BCAST is not used, is proportional both to the number of keys tracked and the number of clients requesting such keys.
&gt; 当不使用BCAST模式时，请确保配置一个合适的Redis记录key数量的最大值。或者使用BCAST模式，这样Redis服务端就不会消耗任何内存。注意，不实用<code>BCAST</code>模式情况下Redis服务端消耗的内存与追踪的key的数量及客户端请求的数量是成比例的。</p>

    </div>
    <footer class="post-footer">
     

     <div class="post-nav">
    <div class="post-nav-next post-nav-item">
    
        <a href="http://www.shutdown.cn/post/redis6.0%E6%96%B0%E7%89%B9%E6%80%A7-%E5%AE%A2%E6%88%B7%E7%AB%AF%E7%BC%93%E5%AD%98%E5%85%B6%E5%9B%9B-%E4%B8%89%E7%A7%8D%E5%AE%A2%E6%88%B7%E7%AB%AF%E7%BC%93%E5%AD%98%E7%9A%84%E5%B7%A5%E4%BD%9C%E6%A8%A1%E5%BC%8F/" rel="next" title="">
        <i class="fa fa-chevron-left"></i> 
        </a>
    
    </div>

    <div class="post-nav-prev post-nav-item">
    
        <a href="http://www.shutdown.cn/post/redis6.0%E7%89%88%E6%9C%AC%E4%BB%8B%E7%BB%8D/" rel="prev" title="">
         <i class="fa fa-chevron-right"></i>
        </a>
    
    </div>
</div>
      
     
     
     






    </footer>
  </article>
</section>

          </div>
        </div>
        <div class="sidebar-toggle">
  <div class="sidebar-toggle-line-wrap">
    <span class="sidebar-toggle-line sidebar-toggle-line-first"></span>
    <span class="sidebar-toggle-line sidebar-toggle-line-middle"></span>
    <span class="sidebar-toggle-line sidebar-toggle-line-last"></span>
  </div>
</div>
<aside id="sidebar" class="sidebar">
  <div class="sidebar-inner">

    <section class="site-overview sidebar-panel  sidebar-panel-active ">
      <div class="site-author motion-element" itemprop="author" itemscope itemtype="http://schema.org/Person">
    <img class="site-author-image" itemprop="image"
        src="http://www.shutdown.cn/img/author.jpg"
        alt="不与天斗Domino" />
    <p class="site-author-name" itemprop="name">不与天斗Domino</p>
    <p class="site-description motion-element" itemprop="description"> 
        Programmer &amp; Architect</p>
</div>
      <nav class="site-state motion-element">
    <div class="site-state-item site-state-posts">
      <a href="http://www.shutdown.cn/post/">
        <span class="site-state-item-count">183</span>
        <span class="site-state-item-name">日志</span>
      </a>
    </div>
    <div class="site-state-item site-state-categories">    
        <a href="http://www.shutdown.cn/categories/">      
         
        <span class="site-state-item-count">15</span>
        
        <span class="site-state-item-name">分类</span>
        
        </a>
    </div>

    <div class="site-state-item site-state-tags">
        <a href="http://www.shutdown.cn/tags/">
         
        <span class="site-state-item-count">224</span>
        
        <span class="site-state-item-name">标签</span>
        </a>
    </div>
</nav>
      
      

      

      <div class="links-of-blogroll motion-element inline">
<script type="text/javascript" src="//rf.revolvermaps.com/0/0/8.js?i=&amp;m=0&amp;s=220&amp;c=ff0000&amp;cr1=ffffff&amp;f=arial&amp;l=33&amp;bv=35" async="async"></script>
</div>

    </section>
    
  </div>
</aside>

      </div>
    </main>
   
    <footer id="footer" class="footer">
      <div class="footer-inner">
        <div class="copyright" >
  <span itemprop="copyrightYear">  &copy; 
  2013 - 2023</span>
  <span class="with-love"><i class="fa fa-heart"></i></span>
  <span class="author" itemprop="copyrightHolder">天地维杰网</span>
  <span class="icp" itemprop="copyrightHolder"><a href="https://beian.miit.gov.cn/" target="_blank">京ICP备13019191号-1</a></span>
</div>
<div class="powered-by">
  Powered by - <a class="theme-link" href="http://gohugo.io" target="_blank" title="hugo" >Hugo v0.63.2</a>
</div>
<div class="theme-info">
  Theme by - <a class="theme-link" href="https://github.com/xtfly/hugo-theme-next" target="_blank"> NexT
  </a>
</div>


      </div>
    </footer>

    <div class="back-to-top">
      <i class="fa fa-arrow-up"></i>
      <span id="scrollpercent"><span>0</span>%</span>
    </div>
  </div>

  

<script type="text/javascript">
  if (Object.prototype.toString.call(window.Promise) !== '[object Function]') {
    window.Promise = null;
  }
</script>
<script type="text/javascript" src="http://www.shutdown.cn/js/vendor/jquery/index.js?v=2.1.3"></script>
<script type="text/javascript" src="http://www.shutdown.cn/js/vendor/fastclick/lib/fastclick.min.js?v=1.0.6"></script> 
<script type="text/javascript" src="http://www.shutdown.cn/js/vendor/jquery_lazyload/jquery.lazyload.js?v=1.9.7"></script>
<script type="text/javascript" src="http://www.shutdown.cn/js/vendor/velocity/velocity.min.js?v=1.2.1"></script>
<script type="text/javascript" src="http://www.shutdown.cn/js/vendor/velocity/velocity.ui.min.js?v=1.2.1"></script>
<script src="http://www.shutdown.cn/js/vendor/ua-parser-js/dist/ua-parser.min.js?v=0.7.9"></script>

<script src="http://www.shutdown.cn/js/vendor/fancybox/jquery.fancybox.pack.js?v=2.1.5"></script>

<script type="text/javascript" src="http://www.shutdown.cn/js/utils.js"></script>
<script type="text/javascript" src="http://www.shutdown.cn/js/motion.js"></script>
<script type="text/javascript" src="http://www.shutdown.cn/js/affix.js"></script>
<script type="text/javascript" src="http://www.shutdown.cn/js/schemes/pisces.js"></script>

<script type="text/javascript" src="http://www.shutdown.cn/js/scrollspy.js"></script>
<script type="text/javascript" src="http://www.shutdown.cn/js/post-details.js"></script>
<script type="text/javascript" src="http://www.shutdown.cn/js/toc.js"></script>

<script type="text/javascript" src="http://www.shutdown.cn/js/bootstrap.js"></script>

<script type="text/javascript" src="http://www.shutdown.cn/js/search.js"></script>
<script type="text/x-mathjax-config">
  MathJax.Hub.Config({
    extensions: ["tex2jax.js"],
    jax: ["input/TeX", "output/HTML-CSS"],
    tex2jax: {
      inlineMath: [ ['$','$'] ],
      displayMath: [ ['$$','$$'] ],
      processEscapes: true
    },
    "HTML-CSS": { fonts: ["TeX"] }
  });
</script>
<script src='https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.5/MathJax.js?config=TeX-AMS-MML_HTMLorMML' async></script>
</body>
</html>